chore: merge current upstream dev into clean Telegram successor - #10
Merged
twoimo merged 12 commits intoJul 25, 2026
Merged
Conversation
…Yeachan-Heo#3109) * feat(agents): give read-only role agents irc and read-only git access `architect`, `planner`, and `critic` pin an explicit `tools:` allow-list, so they only receive the tools they name. That excluded `irc`, and their `bashAllowedPrefixes` allowed only `gjc ralplan --write` / `gjc state`, so every git command was blocked. - add `irc` to the three agents' tool lists (`executor` and generic subagents declare no `tools:` frontmatter and already inherit `irc` from the builtin set, so they are intentionally untouched) - add read-only git prefixes: status, log, show, diff, blame, rev-parse, ls-files. These agents use the workflow bash profile, where `validateMatchedGjcCommand` passes any non-`gjc` command once a prefix matches, so mutating git (commit/push/reset/checkout/branch -D/config) and arbitrary shell stay blocked and the read-only contract holds. - sync AGENTS.md and the shared restricted-bash prompt fragment, which both documented the old `gjc`-only contract - add a behavioral regression test driving the real bundled agent definitions through `checkBashAllowedPrefixes`, rather than only pinning the prefix array `web_search` was already present in all three lists; it is now asserted. * fix(task): keep irc active for subagents instead of hiding it behind discovery `irc` is `loadMode: "discoverable"`, and `tools.discoveryMode` defaults to `"all"`, so the initial-tool filter in `createAgentSession` dropped it from every subagent that does not pin an explicit `tools:` list — notably `executor`. The tool was constructed and reachable, but only after the model spent a `search_tool_bm25` round-trip to find it, which is the wrong tradeoff for a coordination channel the agent needs proactively. - add `CreateAgentSessionOptions.alwaysActiveToolNames`: discoverable built-ins that stay in the initial active set even under `discoveryMode: "all"` - pass `["irc"]` from the subagent executor when the parent runtime reports IRC is actually available (`ircAvailable`), so behavior still follows the existing `irc.enabled` + peer-roster gating - cover it with a session-level regression test asserting irc is hidden by default for a subagent-style session, active when forced, and that forcing one tool does not drag in the rest of the discoverable set Read-only role agents are unaffected: they pass explicit tool names, so irc already survived the filter via the explicitly-requested path. --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…h race (Yeachan-Heo#3111) * test: remediate 11 confirmed-flaky tests and 1 superseded member; fix broker identity publish race Phase 2 of the flaky-CI stabilization program (audit-scoped, approval-gated): - sdk/broker/identity.ts: atomic identity publication via same-directory temp file + hard link with EEXIST reread (real product race reproduced under 4x parallel contention: "Invalid broker identity key") - sdk-broker.test.ts: new concurrent identity-publication regression test - 11 flaky-test rewrites fixing adjudicated root causes (deterministic awaits over fixed sleeps, fixture isolation, lease/heartbeat seams, bounded polling, mkdtemp AuthStorage isolation, scoped timeouts) - session-directory.test.ts: remove one superseded append-only-sidecar member (superseded by retained-cleanup contract, c49c259) Evidence chain: 62-suspect audit (2-week window, 3 reconciliation identities, blinded second validation, third-adjudicator protocol), 24 verification receipts, mutation-probe matrix, 220/220 stress + 80/80 contention iterations. No workflow/branch-protection changes; dev PR CI critical path untouched. * style: apply biome formatting to Phase 2 remediation test edits Formatter-only (line wrapping); no logic change. Fixes the check:@gajae-code/coding-agent CI failure on this branch. --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…sh (Yeachan-Heo#3115) Follow-up to Yeachan-Heo#3109, which gave `architect`/`planner`/`critic` read-only git but had to document a workaround: `git diff HEAD~1` was rejected and had to be quoted as `git diff 'HEAD~1'` (or rewritten as `HEAD^` / an explicit SHA). The restricted-bash parser treated `~` as an unsafe expansion character anywhere in a command. Bash only performs tilde expansion when the tilde opens a word, so `HEAD~1` is a literal argument and the rejection was a false positive against ordinary git revision syntax. `~` is now checked positionally instead of unconditionally: word-initial tildes (`~`, `~/path`, `~user`) are still rejected because bash really does expand them, while a tilde inside a word is allowed. Every other expansion character (`$`, `*`, `?`, `[`, `]`, `{`, `}`), command substitution, control operators, and backslash escapes are unchanged. Verified against real bash: `HEAD~1`, `HEAD~2`, `HEAD~1..HEAD`, `HEAD~5`, and `main...HEAD` all print literally, while `~`, `~/secrets`, and `~root` expand to home directories and therefore stay blocked. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…an-Heo#3117) (Yeachan-Heo#3121) * fix(security): reject bash tilde expansion in assignment words (Yeachan-Heo#3117) Follow-up repair to Yeachan-Heo#3115, which made the restricted role-agent bash parser treat `~` positionally but only rejected it at the start of a whitespace- delimited word. Bash also performs tilde expansion inside an assignment word: directly after the first `=` and after each `:` in the assigned value, so `A=~`, `foo=~root/bar`, `A=x:~`, and `a=x:~:y:~` all expanded while the parser accepted them. `parseShellWords` now tracks per-token assignment state and rejects an unquoted `~` at every position bash can expand it: word start, right after the first unquoted `=` of an assignment word, and right after an unquoted `:` in that word's value. An assignment word requires the raw characters before the first unquoted `=` to match `[A-Za-z_][A-Za-z0-9_]*` with no quoting, so non-assignment tokens (`--opt=~`, `a-b=~`, `1abc=~`, `=~`) and non-expansion positions (`a=x~y`, `a=x:y~z`, `a=b=~`) stay allowed, as do literal mid-word git revisions such as `HEAD~1`. Token state resets at whitespace boundaries. Verified against real bash: `A=~`, `A=x:~` expand to home paths while `a=b=~`, `a=x~y`, and `HEAD~1` stay literal. Parameter expansion, command substitution, control operators, newlines, backslash escapes, and the tilde denial reason string are unchanged. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev> * fix(security): recognize compound `+=` assignment words in restricted bash (Yeachan-Heo#3117) Adversarial review of Yeachan-Heo#3121 found a fail-open bypass: bash(1) defines an assignment word as both `name=value` and `name+=value`, and both tilde-expand in the value, but `ASSIGNMENT_NAME_PATTERN` was tested against the region before the first unquoted `=`, which holds `A+` for `A+=`. `A+` failed the name pattern, so `tildeExpandable` was never armed and `A+=~`, `A+=x:~`, and `A+=~root/bar` were allowed through despite real bash expanding all three. The assignment-name pattern now accepts an optional single trailing `+`, so a compound assignment word arms tilde expansion on exactly the same positions as a plain one, including the `:` continuation in the value. Nothing else changes: `a++=~`, `+=~`, and `a+b=~` still fail name validation and stay allowed, as do `a+=x~y` and `a+=b=~` where bash does not expand. Verified against bash 5.1.16 with HOME=/HOMEDIR: `A+=~` and `A+=x:~` expand, while `a+=x~y`, `a+=b=~`, `a++=~`, `+=~`, and `a+b=~` stay literal. The denial reason string is unchanged. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev> --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…eo#3122) * ci: add non-required rehearsal dispatch mode to release CI The stabilization plan requires proving the release graph is green without waiting for a real tag (tags are non-deterministic and must not gate a soak). Every ci.yml job is already switched on `startsWith(github.ref, 'refs/tags/v')`, so a workflow_dispatch rehearsal mode reuses the EXACT production job definitions in place — no reusable-workflow extraction, no drift risk, and job IDs / check context names are unchanged. - `workflow_dispatch` input `rehearsal`: tag-build-verify | main-nontag - tag-graph jobs (native, binaries) additionally run under dispatch:tag-build-verify; non-tag jobs under dispatch:main-nontag - publish stays tag-only AND is explicitly excluded from dispatch (`github.event_name != 'workflow_dispatch'`), so NPM_TOKEN — referenced only inside publish, with no `secrets: inherit` anywhere — is unreachable from any rehearsal - concurrency group gains a dispatch-mode suffix so a rehearsal can never cancel a real release run Behavior for push-main, pull_request, and push-tag is unchanged: the added clauses only constrain workflow_dispatch. Release-policy and publish-order guard tests updated accordingly (35 tests green). * test(ci): expect test:@gajae-code/stats in the native-change push plan packages/stats has a `test` script and a real test/ directory, so the planner correctly emits test:@gajae-code/stats for full push plans. The hardcoded expectation in the native-workspace planning test was stale and fails on clean dev — this PR's workflow edit merely routes the selftest selector into the plan, which surfaced it. Verified: fails identically at origin/dev without this change. --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…view state (Yeachan-Heo#3127) Two defects made the deep-interview workflow unusable in practice. **Guard blocked `/dev/null` redirects.** `extractBashTargets` captured every `>`/`>>` target, so `cmd 2>/dev/null` was treated as a repository write and blocked during any planning phase — including the brownfield exploration deep-interview itself mandates before Round 1. Only `/dev/null` is exempted. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>` stay blocked: they are descriptor aliases, and `exec 1<>src/product.ts; printf x >/dev/stdout` reaches a real file through a rebound descriptor. Suppressing a sink target must not turn a mutating command into an empty-target "safe" verdict, so this also closes the bypasses that suppression would otherwise open: - `exec` redirections fail closed (descriptor rebinding is not statically resolvable) - `>|`, `>&path`, and `<>` write forms are recognized - path-qualified writers (`/bin/dd`, `/usr/bin/tee`) are matched - every `dd of=` operand is inspected, not just the first (GNU dd honors the last) - a redirect capture that dequotes to nothing is `unknown`, not safe **Hook-seeded state failed native validation.** The draft CLI ran `validateDeepInterviewV1Envelope` whenever `verifyWorkflowEnvelopeReceiptValue` returned `native-valid`, but that verdict covers receipt shape and checksum only — it does not imply a native v1 body. The hook's minimal `ModeState` has no `schema_version` and no `state`, so every typed operation failed with `DI_STATE_SCHEMA_INVALID` and interviews ran fully manually with `rounds: []`. Gate on `isNativeDeepInterviewV1` instead, matching the already-correct `transformGuardedWorkflowEnvelopeAtomic`, and seed a native v1 envelope from the hook. The seed omits `initial_idea`: pre-seeding `""` made a later `initialize-context` carrying a real idea fail with `DI_SETUP_CONFLICT`. Round shells now canonicalize the agent-supplied `deepInterview.dimension` label, which is free text on post-topology asks but must be a canonical id in persisted state. .gjc/** writes remain blocked in every case. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
* Keep ACP eval execution inside the established permission boundary Eval-triggered JavaScript and Python tool dispatch must pass through the same prepared session tool path as model-issued calls, so ACP policy remains authoritative while recursion and lifecycle ownership stay centralized. This preserves the accepted implementation direction from the closed contribution chain without unrelated repairs. Constraint: Fresh replacement is based exactly on upstream/dev e3c7f6a. Constraint: Publication remains gated on terminal-success exact-dev CI plus explicit owner authorization. Rejected: Add a second eval-specific authorization layer | it would duplicate policy and risk disagreement with the canonical session boundary. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep nested eval tool dispatch routed through getToolForExecution; raw registry lookup would bypass ACP session policy. Tested: Exact six-file ACP/SDK/Python integration set (98 tests, 488 assertions); coding-agent Biome and TypeScript checks; operation inventory generation and --check; native addon build; production binary build; root/source and compiled-binary CLI smokes; isolated compiled-binary and source-linked install smokes; git diff, stable patch-id, and merge-tree checks. Not-tested: Hosted-only Linux x64 tarball install substep requiring the CI-provided pi_natives.linux-x64 addon. Related: Yeachan-Heo#2737, Yeachan-Heo#2980, Yeachan-Heo#2992 * docs(changelog): move ACP eval permission entry into Unreleased The Yeachan-Heo#2737 sweep note required the changelog entry to land in the current Unreleased/Fixed section. At head 0ae2315 it was appended to the already released [0.11.8] block instead, retroactively editing a shipped release section. Moved verbatim into [Unreleased] > Fixed; no other change. Co-authored-by: Oreochococukie <Oreochococukie@users.noreply.github.com> --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev> Co-authored-by: Oreochococukie <Oreochococukie@users.noreply.github.com>
The workflow-level `permissions: contents: write` grant was redundant: `publish` is the only job that needs write and already declares its own job-level `contents: write` override. The workflow-level grant handed a write-scoped GITHUB_TOKEN to every build/verify job, including the new rehearsal dispatches, which only check out and build. No job outside `publish` references GITHUB_TOKEN, github.token, `gh`, or `git push`, so narrowing the default is behavior-neutral for real push/PR/tag runs while removing unnecessary write scope from rehearsals. Surfaced by an independent audit of the stabilization program. Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…ancellation/timeout regression (successor to Yeachan-Heo#3112) (Yeachan-Heo#3131) * fix(vendor/insane-search): P0 — 429 rate-limit no longer kills fetch + bias_check violations Problem 1: RATE_LIMITED was in TERMINAL_NONSUCCESS, causing a single 429 on the probe or any grid candidate to immediately terminate the entire fetch pipeline — no grid diversity, no browser fallback, no backoff. Fix: - Remove RATE_LIMITED from TERMINAL_NONSUCCESS (validators.py) - Add _rate_limit_backoff() with linear escalation (2s→10s cap) and Retry-After header support (fetch_chain.py) - Grid now skips rate-limited candidates and continues to the next TLS/referer combo instead of breaking - Browser fallback is no longer skipped on 429 Problem 2: bias_check CI gate reported violations for WAF vendor domains (funcaptcha.com) and Jina Reader (r.jina.ai) used as infrastructure. Fix: - Add funcaptcha.com, api.funcaptcha.com, r.jina.ai, jina.ai to URL_ALLOWLIST in bias_check.py with explanatory comments Verified: - bias_check → clean - TERMINAL_NONSUCCESS == {auth_required, not_found} (429 excluded) * fix(vendor/insane-search): repair two NameErrors in SessionPool.warmup + record local patches engine/transport.py SessionPool.warmup() referenced two names that do not exist in its scope: 1. allow_private — computed inline into classify_url() but never bound, then passed to _fetch_following() on the next line. 2. DEFAULT_MAX_REDIRECTS — lives in engine.safety; the sibling call site at get_or_create() already qualifies it as safety.DEFAULT_MAX_REDIRECTS. Every warmup() call that passed the safety gate raised NameError, so the per-(host, impersonate) root warmup — the step that lets a WAF sensor set a resolved session cookie before the deep request — never actually ran. engine/tests/test_u4.py failed on the vendored tree because of this. Also records all three local fixes in MANIFEST.json localPatches, per the vendoring convention, so a future re-vendor from upstream does not silently drop them. Verified: - engine/tests: 8/8 files pass (was 7/8) - engine/bias_check.py: clean - bun run ci:check:full: exit 0 * fix(vendor/insane-search): pass resp to _rate_limit_backoff on all 429 paths + probe backoff + tests Review findings addressed (PR Yeachan-Heo#3104): 1. _rate_limit_backoff(resp) now receives the actual response on every 429 path (grid AND probe), so server-provided Retry-After is read and honoured (numeric, capped at 30s). Previously resp was discarded. 2. Probe-phase 429 now triggers backoff before entering the grid, preventing the first grid candidate from hitting the same rate window. 3. Deterministic regression tests (engine/tests/test_rate_limit_backoff.py): - Retry-After header read + cap at 30s - Probe-phase 429 backoff - Grid-phase 429 passes resp - Repeated 429s bounded by attempt budget, terminate correctly - 429 does not defeat Playwright browser fallback - Linear escalation (base*1..base*5 cap) All sleeps mocked; no real delays. 4. transport.py: SECURITY NOTE clarifies the SSRF guard is resolver-level (pre-connect) only — NOT transport-bound DNS validation. Does not claim equivalence to DNS rebinding or redirect-to-private authority protection. 5. MANIFEST.json localPatches entry updated to reflect the Retry-After fix and test coverage. * fix(vendor/insane-search): clamp/validate rate-limit backoff base + cancellation/timeout regression Addresses REQUEST_CHANGES on Yeachan-Heo#3112 (bounded/cancellable backoff contract). P0: _rl_base = float(INSANE_RATE_LIMIT_BACKOFF_S) was neither validated nor capped. Verified failure modes: a non-numeric value raised ValueError inside _fetch_core; NaN produced an undefined time.sleep; inf hung time.sleep forever (defeating any timeout); a negative value raised ValueError in time.sleep; a huge value bypassed the advertised 30s bound (the cap only applied to the Retry-After header, not the base-derived delay). Fix: - Add fetch_chain._clamp_rate_limit_base(): a total parser that rejects non-numeric/NaN/infinite/negative values (falls back to the 2s default) and clamps a valid value to the 30s ceiling (_RATE_LIMIT_MAX_DELAY). - Hard-cap the final per-attempt delay to _RATE_LIMIT_MAX_DELAY with a finite/non-negative guard before time.sleep, so the bound holds end-to-end even for a huge-but-finite base, and sleeps stay short enough that an abort/KeyboardInterrupt between attempts is honoured promptly (cancellable). Tests (test_rate_limit_backoff.py): - The header claimed abort/timeout coverage but contained none. Fixed the header and added the missing deterministic regressions: * cancellation: KeyboardInterrupt raised during a backoff sleep propagates immediately and cuts the chain short (backoff does not swallow the abort); * timeout: no single sleep exceeds 30s or becomes non-finite even under a huge base AND a huge Retry-After, so a per-attempt deadline can rely on it; * base-validation: helper unit test + end-to-end huge/negative/NaN/inf/ non-numeric env cases. - All new tests fail/error against the unfixed code and pass with the fix (14 tests total, all deterministic, mocked sleep). Provenance: kept the MANIFEST.json local-patch entry and documented the hardening accurately. Boundary: the disabled production TypeScript bridge is intentionally untouched — DNS authority is not transport-bound, so that boundary stays closed. Gates: test_rate_limit_backoff.py (14), test_hardening.py (4), bias_check.py (clean), scripts/verify-insane-vendor.ts (passed). --------- Co-authored-by: dmae97 <dmae97@users.noreply.github.com> Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…4 migration before publishing successor identity (Yeachan-Heo#3080) Gate the successor `'/tmp/gjc-local/019f97df-38e7-7000-a304-bd5a7ea8e374'` root before the successor session identity is published, across `/new` (both lease branches), `fork()`, handoff, `/resume` switchSession, and branch/tree-jump. Adversarial exact-head review approved at review #4778681080 for head a1878d7. Verification at merge time: - Per-site rotation -> gate -> publication ordering confirmed at all six sites. - Exhaustive adjudication of the full #beginSessionTransition set: clear-context, navigate-tree, and compact perform no identity rotation and correctly need no gate. - Order-inversion mutation testing proved the added invariant test is non-vacuous (fails with markerExists=false when the gate is moved after identity publication on the covered path). - Focused suites 13/13 and 66/66 pass, deterministic across 3 runs; coding-agent typecheck clean. - Exact-head CI green on a1878d7 (15 success, 5 conditional skips, 0 failures). - Current dev da7cc11 CI terminal success (24 success, 5 skipped, 0 failures). - merge-tree against current dev: no conflicts; dev's two intervening commits touch disjoint files. Non-blocking follow-ups noted in review: the pre-existing cleanup_pending sync/async marker asymmetry in internal-urls (untouched by this PR), and order-inversion regression coverage for the five gated sites the new test does not exercise.
* feat(models): repoint opus presets to claude-opus-5 Anthropic shipped claude-opus-5 (2026-07-24) with the same published envelope as claude-opus-4-8: 1M context, 128k output, effort range low..max, 5/25 pricing. Repoint the opus-related built-in presets to it. - claude-opus: default/planner/critic/architect -> anthropic/claude-opus-5 with existing effort suffixes; executor stays anthropic/claude-sonnet-5 - opus-codex: default -> anthropic/claude-opus-5:xhigh; the durable anthropic/claude-sonnet-5 planner override is unchanged - fable-opus-codex: planner/critic -> anthropic/claude-opus-5; the fable default and codex roles are unchanged - Regenerate packages/ai/src/models.json so anthropic/claude-opus-5 resolves; zero providers or models removed - Extend the generator's Claude Opus vision normalization to opus-5, which upstream shipped without image input on three variants, and replace the duplicated substring guard with a shared exact-generation parser plus a tripwire test that fails when the catalog bundles an Opus generation newer than any reviewed one - Realign preset expectation tests and update docs; measured opus-4-8 figures in docs/multi-vendor-profiles.md stay attributed to opus-4-8 rather than silently transferring to an unmeasured model * fix(models): remove unrelated opus preset catalog drift --------- Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
twoimo
merged commit Jul 25, 2026
cff6567
into
successor/telegram-tool-activity-clean
48 of 77 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Internal synchronization PR. This merges current upstream
devinto the preserved Telegram successor without squashing either history.